#!/usr/bin/python
'''Fix declaration of objects to pass all required arguments and 
allow an override to the output name for the artist, delimiter, fix
verbosity to follow through.  Also fix up the AVFilename by model 
and try and make that usable as well.

Todo: Function to take a content number and figure out episode number
versus season number.  May be worthwhile to capture it in S??E?? directly
and add support for SSxEE numbering format.
'''

import common
import datetime
import logging
import os
import re

# split into two, music and video, and then create this by joining them
EXTENSIONS = ['.mp3', '.avi', '.mpeg', '.mpg', '.ogm', '.m4a', '.wma', '.wmv', 
              '.mkv', '.flac', '.ogg', '.mov', '.m4v']

                    
module_logger = common.setupModuleLogger(__name__)


def getFilenameParts(filename):
  '''Takes a filename and returns the basename and extension'''
  chop_pt = filename.rfind('.')
  if chop_pt != -1:
    return filename[:chop_pt], filename[chop_pt:]


def hasMediaExt(filename):
  no_ext_warning = 'Filename "%s" has no file extension.'
  bad_ext_warning = 'Filename "%s": "%s" not a media extension.'
  try:
    _, ext = getFilenameParts(filename) 
  except TypeError:
    #no extension to judge on, have to fail it, TODO: build override.
    module_logger.warning(no_ext_warning % filename)
    return False
  if ext in EXTENSIONS:
    return True
  else:
    module_logger.warning(bad_ext_warning % (filename, ext))


class AVFile(object):
  def __init__(self, filename, directory=None, blacklist=None,
               verbose=20, delim=' ', artist_override=None):
    """Base object representing a AV filename.
    TODO(gnudan): Fix verbose attribute to set logger level.
    TODO(gnudan): Determine plan for optional attributes, like
        release date, and season and episode numbers (aka why parse
        so finely if we throw that distinction away?).
    """
    self.logging = self.setupLogging()
    self.logging.setLevel(verbose)
    try:
      self.basename, self.extension = getFilenameParts(filename)
    except TypeError:
      self.extension = ''
      self.basename = filename
    if blacklist:
      self.basename = self.removeBlackListed(blacklist)
    if artist_override:
      self.artist_title = artist_override
    else:
      self.artist_title = ''
    self.content_num = ''
    self.content_title = ''
    self.delim = delim
    self.directory = directory

  def setupLogging(self):
    return logging.getLogger('%s.%s' % (self.__module__,
                                        self.__class__.__name__))

  def normalizePunctuation(self, name_section, delim=None):
    """Removes edge punctuation and normalizes all other punctuation."""
    if not delim:
      delim = self.delim
    self.logging.debug('Name before normalizePunctuation: %s' % name_section)
    name_section = self.removeEdgePunctuation(name_section)
    normalize_reg = '[\W_]+'
    normalize_regex = re.compile(normalize_reg)
    normal_name = normalize_regex.sub(delim, name_section)
    self.logging.debug('Name after normalizePunctuation: %s' % normal_name)
    return normal_name

  def removeEdgePunctuation(self, name_section):
    """Removes edge punctuation from a name or name segment."""
    self.logging.debug('Before removeEdgePunctuation: %s' % name_section)
    edge_reg = '^[\W_]+|[\W_]+$'
    edge_regex = re.compile(edge_reg)
    cleaned = edge_regex.sub('', name_section)
    self.logging.debug('After removeEdgePunctuation: %s' % cleaned)
    return cleaned
  
  def parseFilename(self):
    '''Override me'''
    pass

  def printObj(self):
    print "Basename: %s \nExt: %s \n" % (self.basename,
                                          self.extension)
    print ("Artist Title: %s\n"
           "Content Title: %s\n"
	   "Content Number: %s" % (self.artist_title,
	                           self.content_title,
				   self.content_num))

  def fullNameOut(self, model='%a-%n-%c', title=True):
    """
    TODO(gnudan): What if one of the keys exists in the model but not in the
        basename?  We get a '' or None in the list that gets joined by the
        delim? This will need to be a working case, as optional file parts are
        coming.
    """
    match_table = { '%a' : self.artist_title,
                    '%c' : self.content_title,
                    '%n' : self.content_num
                  }
    output_order = []
    order_by_pos = {}
    for key in match_table:
      key_pos = model.find(key)
      if key_pos == -1:
        continue
      elif match_table[key]:
        order_by_pos[key_pos] = match_table[key]
    positions_map = order_by_pos.keys()
    positions_map.sort()
    for pos in positions_map:
      output_order.append(order_by_pos[pos])
    new_name = self.delim.join(output_order)
    if title:
      new_name = new_name.title()
    if self.directory:
      new_name = os.path.sep.join([self.directory, new_name])
    if self.extension:
      new_name = "%s%s" % (new_name, self.extension)
    return new_name

  def newNameOut(self, output_model, title=True, upper=False, lower=False):
    match_table = { '%a' : self.artist_title,
                    '%c' : self.content_title,
                    '%n' : self.content_num
                  }
    new_name = output_model
    for key in match_table:
      if key in new_name and match_table[key]:
        new_name = new_name.replace(key, match_table[key])
      else:
        # Remove the key from the output string and optionally one separating
        # character between the current key and the one preceding it.
        regex = re.compile('[\W_]?%s' % key)
        new_name = regex.sub('', new_name)
    new_name = self.removeEdgePunctuation(new_name)
    if title:
      new_name = new_name.title()
    elif upper:
      new_name = new_name.upper()
    elif lower:
      new_name = new_name.lower()
    return new_name
    
  def fullPathOut(self, output_model, title=True, upper=False, lower=False):
    new_name = self.newNameOut(output_model,
                               title=title,
                               upper=upper,
                               lower=lower)
    if self.directory:
      new_path = os.path.sep.join([self.directory, new_name])
    if self.extension:
      new_path = "%s%s" % (new_path, self.extension)
    return new_path
    
  def removeBlackListed(self, blacklist_file):
    """Removes items in the blacklist file, case insensitive from the filename.
    
    Args:
      blacklist_file: A filename or path to a text file of black listed items,
          one per line.  Only plain words are supported, regex support is
          planned.
    TODO(gnudan): Add regex support for individual lines.
    TODO(gnudan): Support very long black lists.
    """
    black_listed = open(blacklist_file).readlines()
    black_listed = map(str.strip, black_listed)
    black_listed = filter(None, black_listed)
    black_listed = map(re.escape, black_listed)
    black_listed = ['[\W_]?%s[\W_]?' % word for word in black_listed]
    redact_reg = '|'.join(black_listed)
    regex = re.compile(redact_reg, re.I)
    new_name = re.sub(regex, '', self.basename)
    self.logging.info('%s -> %s' % (self.basename, new_name))
    return new_name


class AVFileWithNumber(AVFile):
  ''' Object represents a AV file's filename and parses info out of the filename.
  
  Todo: separate flow into video and audio to avoid matching things that don't fit.
  Todo: season/episode and date numbers only make sense as unique numbers for video.
        does music vs. video need a flag, or should it be checked once and set?
  Todo: Need a date delimiter vs. the regular delimiter vs. a section delim.
        But only two of the above.
  '''
  ENUM = r'(?P<episode_num>[0-9]+|_[0-9]+)'
  SE_NUM = r'(?P<season_episode_num>[\w]{1}[0-9]{1,2}[\w]{1}[0-9]{1,2})'
  DATE_NUM = r'(?P<date_num>[0-9]{4}([\W_]{1}[0-9]{1,2}){2})'
  R_DATE_NUM = r'(?P<r_date_num>([0-9]{1,2}[\W_]{1}){2}[0-9]{4})'
  DATE_STR = r'(?P<date_str>[0-9]{1,2}[\W_]?[a-zA-Z]{3}[\W_]?[0-9]{4})'
  R_DATE_STR = r'(?P<r_date_str>[0-9]{4}[\W_]?[a-zA-Z]{3}[\W_]?[0-9]{1,2})'

  def __init__(self, filename, directory=None, blacklist=None, 
               verbose=False, delim=' ', artist_override=None):
    super(AVFileWithNumber, self).__init__(filename, directory,
                                           blacklist=blacklist,
                                           verbose=verbose,
                                           delim=delim,
                                           artist_override=artist_override)
    self.content_num_raw = ''
    self.logging.debug('Before child processing: %s' % self.basename)
    self.content_num = self.findContentNumber()
    # error if content_num_raw never gets set
    

  def parseFilename(self):
    # should some model to make this judgement be developed?
    cnum_start = self.basename.index(self.content_num_raw)
    cnum_stop = cnum_start + len(self.content_num_raw)
    self.content_title = self.normalizePunctuation(self.basename[cnum_stop:])
    if not self.artist_title:
      self.artist_title = self.normalizePunctuation(self.basename[:cnum_start])

  def findContentNumber(self):
    """Function works to find a number somewhere in the filename.

    Namely the selection latches onto patterns of the content number.
    The order of the regex strings in attempting to match is crucial. 

    Todo: remove return statements.
    """
    reg_dict = { 'date_num': self.DATE_NUM,
                 'r_date_num': self.R_DATE_NUM,
                 'date_str': self.DATE_STR,
                 'r_date_str': self.R_DATE_STR,
                 'season_episode_num': self.SE_NUM,
                 'episode_num': self.ENUM }

        # Try to match a number in order of specificity.
    for reg in ('date_num', 'r_date_num', 'date_str', 'r_date_str',
                'season_episode_num', 'episode_num'):
      regex = re.compile(reg_dict[reg])
      match = regex.search(self.basename)
      if match:
        self.logging.debug('%s matched number pattern %s' % (self.basename,
                                                            match.groupdict()))
        break
    else:
      # TODO(gnudan) Change this to return None.
      raise Exception, "No content number found."
    if reg == 'season_episode_num':
      # set season/episode numbers here
      self.content_num_raw = match.group(reg)
      # compose number from season/episode numbers instead.
      snum = [ letter for letter in match.group('season_episode_num')
               if letter.isdigit() ]
      if snum[0] == '0' or snum[0] == 0:
        snum = snum[1:]
      return ''.join(snum)
    else:
      self.content_num_raw = match.group(reg)
      # TODO(gnudan) Warning hardcode HERE! Fix me!
      return self.normalizePunctuation(match.group(reg), delim='-')


class ContentNumberFactory(object):
  """Represent numbers in filenames to be able to change formats."""
  ENUM = r'(?P<episode_num>[0-9]+|_[0-9]+)'
  SE_NUM = r'(?P<season_episode_num>[\w]{1}[0-9]{1,2}[\w]{1}[0-9]{1,2})'
  DATE_NUM = r'(?P<date_num>([0-9]{1,2}[\W_]{1}){2}[0-9]{4})'
  R_DATE_NUM = r'(?P<r_date_num>[0-9]{4}([\W_]{1}[0-9]{1,2}){2})'
  DATE_STR = r'(?P<date_str>[0-9]{1,2}[\W_]?[a-zA-Z]{3}[\W_]?[0-9]{4})'
  R_DATE_STR = r'(?P<r_date_str>[0-9]{4}[\W_]?[a-zA-Z]{3}[\W_]?[0-9]{1,2})'
  
  REG_MATCH_ORDER = ('date_num', 'r_date_num', 'date_str', 'r_date_str',
                     'season_episode_num', 'episode_num')

  def __init__(self, filename, verbose=20):
    self.filename = filename
    self.logging = self.setupLogging()
    self.logging.setLevel(verbose)

  def setupLogging(self):
    return logging.getLogger('%s.%s' % (self.__module__,
                                        self.__class__.__name__))
  def DeclareContentNumber(self):
    reg_dict = { 'date_num': self.DATE_NUM,
                 'r_date_num': self.R_DATE_NUM,
                 'date_str': self.DATE_STR,
                 'r_date_str': self.R_DATE_STR,
                 'season_episode_num': self.SE_NUM,
                 'episode_num': self.ENUM }
    for reg in self.REG_MATCH_ORDER:
      regex = re.compile(reg_dict[reg])
      match = regex.search(self.filename)
      if match:
        self.logging.debug('%s matched number pattern %s' % (self.filename,
                                                            match.groupdict()))
        break
    else:
      return None
    if reg == 'date_num':
      return ContentDate(match.group(reg))
    elif reg == 'date_str':
      return ContentDate(match.group(reg), month_string=True)
    elif reg == 'r_date_num':
      return ContentDate(match.group(reg), year_first=True)
    elif reg == 'r_date_str':
      return ContentDate(match.group(reg), year_first=True, month_string=True)
    elif reg == 'season_episode_num':
      return SeasonEpisodeNumber(match.group(reg))
    elif reg == 'episode_num':
      return EpisodeNumber(match.group(reg))
    else:
      return None


class ContentNumber(object):
  def GetSeasonNumber(self):
    """For three and more digit numbers find the season number."""
    pass

  def GetContentNumber(self):
    pass


class ContentDate(ContentNumber):
  def __init__(self,
               content_str,
               month_string=False,
               month_str_first=False,
               year_first=False,
               **options):
    self.raw_content_str = content_str
    self.month_str = month_string
    self.month_str_first = month_str_first
    self.year_first = year_first
    self.options = options
  
  def FindDatePunctuation(self):
    punc = re.sub('\w', '', self.raw_content_str)
    if not punc:
      return ('', '')
    elif len(punc) == 2:
      return (punc[0], punc[1])
    else:
      raise Exception, 'Unexpected date punctuation: %s' % punc

  def MatchDate(self, date_format):
    try:
      parsed_date = datetime.datetime.strptime(self.raw_content_str,
                                               date_format)
    except ValueError:
      raise ValueError, 'Error parsing date %s with %s' % (self.raw_content_str,
                                                           date_format)
    return parsed_date

  def ParseContentDate(self):
    delim1, delim2 = self.FindDatePunctuation()
    MONTH_FIRST = '%m', delim1, '%d', delim2, '%Y'
    DAY_FIRST = '%d', delim1, '%m', delim2, '%Y'
    YEAR_FIRST = '%Y', delim1, '%m', delim2, '%d'
    MONTH_STR_FIRST = '%b', delim1, '%d', delim2, '%Y'
    MONTH_STR = '%d', delim1, '%b', delim2, '%Y'
    YEAR_FIRST_MONTH_STR = '%Y', delim1, '%b', delim2, '%d'

    if not (self.year_first or self.month_str or self.month_str_first):
      # TODO(gnudan) Make this order configurable.
      for date_fmt in (MONTH_FIRST, DAY_FIRST):
        date_format = ''.join(date_fmt)
        try:
          date_obj = self.MatchDate(date_format)
          break          
        except ValueError:
          continue
      else:
        raise Exception, 'Failed to match date %s with %s' % (
            self.raw_content_str, ' or '.join((DAY_FIRST, MONTH_FIRST)))
    else:
      if self.year_first and self.month_str:
        date_format = ''.join(YEAR_FIRST_MONTH_STR)
      elif self.year_first:
        date_format = ''.join(YEAR_FIRST)
      elif self.month_str and self.month_str_first:
        date_format = ''.join(MONTH_STR_FIRST) 
      elif self.month_str:
        date_format = ''.join(MONTH_STR)
      else:
        raise Exception, 'Invalid state, not supposed to get here.'
      try:
        date_obj = self.MatchDate(date_format)
      except ValueError:
        raise ValueError, 'Failed to match date %s with any of: %s' % (
            self.raw_content_str, ', '.join((YEAR_FIRST_MONTH_STR,
                                            YEAR_FIRST,
                                            MONTH_STR_FIRST,
                                            MONTH_STR)))
    return date_obj
